Support mounting /sysroot (and /boot) read-only
authorColin Walters <walters@verbum.org>
Wed, 3 Oct 2018 14:57:19 +0000 (14:57 +0000)
committerColin Walters <walters@verbum.org>
Wed, 11 Dec 2019 15:33:57 +0000 (15:33 +0000)
We want to support extending the read-only state to cover `/sysroot`
and `/boot`, since conceptually all of the data there should only
be written via libostree.  Or at least for `/boot` should *mostly*
just be written by ostree.

This change needs to be opt-in though to avoid breaking anyone.

Add a `sysroot/readonly` key to the repository config which instructs
`ostree-remount.service` to ensure `/sysroot` is read-only.  This
requires a bit of a dance because `/sysroot` is actually the same
filesystem as `/`; so we make `/etc` a writable bind mount in this case.

We also need to handle `/var` in the "OSTree default" case of a bind
mount; the systemd generator now looks at the writability state of
`/sysroot` and uses that to determine whether it should have the
`var.mount` unit happen before or after `ostree-remount.service.`

Also add an API to instruct the libostree shared library
that the caller has created a new mount namespace.  This way
we can freely remount read-write.

This approach extends upon in a much better way previous work
we did to support remounting `/boot` read-write.

Closes: https://github.com/ostreedev/ostree/issues/1265
12 files changed:
Makefile-switchroot.am
apidoc/ostree-sections.txt
src/boot/ostree-remount.service
src/libostree/libostree-devel.sym
src/libostree/ostree-impl-system-generator.c
src/libostree/ostree-sysroot-cleanup.c
src/libostree/ostree-sysroot-deploy.c
src/libostree/ostree-sysroot-private.h
src/libostree/ostree-sysroot.c
src/libostree/ostree-sysroot.h
src/ostree/ot-main.c
src/switchroot/ostree-remount.c

index b81b843febbc2ed21c5e710d9a4531cd63e11ab9..ad370eb79d60b215218521c1d09bfba993109c30 100644 (file)
@@ -55,7 +55,8 @@ ostree_remount_SOURCES = \
     src/switchroot/ostree-mount-util.h \
     src/switchroot/ostree-remount.c \
     $(NULL)
-ostree_remount_CPPFLAGS = $(AM_CPPFLAGS) -Isrc/switchroot
+ostree_remount_CPPFLAGS = $(AM_CPPFLAGS) $(OT_INTERNAL_GIO_UNIX_CFLAGS) -Isrc/switchroot -I$(srcdir)/libglnx
+ostree_remount_LDADD = $(AM_LDFLAGS) $(OT_INTERNAL_GIO_UNIX_LIBS) libglnx.la
 
 if BUILDOPT_SYSTEMD
 ostree_prepare_root_CPPFLAGS += -DHAVE_SYSTEMD=1
index f99c4df55f04d459427806bf9fb842056b195c4d..1ef4bbf638b07ffb7448be31cba51a8027c6178f 100644 (file)
@@ -499,6 +499,7 @@ ostree_sepolicy_get_type
 OstreeSysroot
 ostree_sysroot_new
 ostree_sysroot_new_default
+ostree_sysroot_initialize
 ostree_sysroot_get_path
 ostree_sysroot_load
 ostree_sysroot_load_if_changed
@@ -508,6 +509,8 @@ ostree_sysroot_lock_async
 ostree_sysroot_lock_finish
 ostree_sysroot_unlock
 ostree_sysroot_unload
+ostree_sysroot_set_mount_namespace_in_use
+ostree_sysroot_is_booted
 ostree_sysroot_get_fd
 ostree_sysroot_ensure_initialized
 ostree_sysroot_get_bootversion
index b98110c2d6d9f92ebe739288b05a2886faaf22d5..4c3ed94a425c3a3a5256291619508575e8245279 100644 (file)
@@ -22,12 +22,12 @@ DefaultDependencies=no
 ConditionKernelCommandLine=ostree
 OnFailure=emergency.target
 Conflicts=umount.target
-After=-.mount
+# Run after core mounts
+After=-.mount var.mount
 After=systemd-remount-fs.service
+# But we run *before* most other core bootup services that need write access to /etc and /var
 Before=local-fs.target umount.target
-# Other early boot units that need to write to /var
 Before=systemd-random-seed.service plymouth-read-write.service systemd-journal-flush.service
-# tmpfiles.d usually needs write access to a few places
 Before=systemd-tmpfiles-setup.service
 
 [Service]
index a5d15e93e3462f243b47800487596840712129b8..d1666176d83bbf56383a44b088d69f0ac3a29327 100644 (file)
@@ -19,6 +19,9 @@
 
 /* Add new symbols here.  Release commits should copy this section into -released.sym. */
 LIBOSTREE_2019.7 {
+  ostree_sysroot_initialize;
+  ostree_sysroot_is_booted;
+  ostree_sysroot_set_mount_namespace_in_use;
 } LIBOSTREE_2019.6;
 
 /* Stub section for the stable release *after* this development one; don't
index ce40a698c4e7e18fabefbb91f3c333d797fac5e5..cb9170b3305687626236c1b8255deca61bf7daa1 100644 (file)
@@ -28,6 +28,7 @@
 #ifdef HAVE_LIBMOUNT
 #include <libmount.h>
 #endif
+#include <sys/statvfs.h>
 #include <stdbool.h>
 #include "otutil.h"
 
@@ -189,8 +190,6 @@ _ostree_impl_system_generator (const char *ostree_cmdline,
                                "[Unit]\n"
                                "Documentation=man:ostree(1)\n"
                                "ConditionKernelCommandLine=!systemd.volatile\n"
-                               /* We need /sysroot mounted writable first */
-                               "After=ostree-remount.service\n"
                                "Before=local-fs.target\n"
                                "\n"
                                "[Mount]\n"
index ef95d13c6b7b8b47dd5933912cb422c0f806617d..71d978e00598dd6906af32e10826ce6c47f12873 100644 (file)
@@ -455,6 +455,9 @@ ostree_sysroot_cleanup_prune_repo (OstreeSysroot          *sysroot,
   OstreeRepo *repo = ostree_sysroot_repo (sysroot);
   const guint depth = 0; /* Historical default */
 
+  if (!_ostree_sysroot_ensure_writable (sysroot, error))
+    return FALSE;
+
   /* Hold an exclusive lock by default across gathering refs and doing
    * the prune.
    */
@@ -535,7 +538,10 @@ _ostree_sysroot_cleanup_internal (OstreeSysroot              *self,
                                   GError                    **error)
 {
   g_return_val_if_fail (OSTREE_IS_SYSROOT (self), FALSE);
-  g_return_val_if_fail (self->loaded, FALSE);
+  g_return_val_if_fail (self->loadstate == OSTREE_SYSROOT_LOAD_STATE_LOADED, FALSE);
+
+  if (!_ostree_sysroot_ensure_writable (self, error))
+    return FALSE;
 
   if (!cleanup_other_bootversions (self, cancellable, error))
     return glnx_prefix_error (error, "Cleaning bootversions");
index a09c354b93534f509430e3d96c73dd0401765562..0dcda7328f1b420f2a02c1f6b13c6a791d3ba465 100644 (file)
@@ -56,6 +56,9 @@
 #define OSTREE_DEPLOYMENT_FINALIZING_ID SD_ID128_MAKE(e8,64,6c,d6,3d,ff,46,25,b7,79,09,a8,e7,a4,09,94)
 #endif
 
+static gboolean
+is_ro_mount (const char *path);
+
 /*
  * Like symlinkat() but overwrites (atomically) an existing
  * symlink.
@@ -806,6 +809,9 @@ write_origin_file_internal (OstreeSysroot         *sysroot,
                             GCancellable          *cancellable,
                             GError               **error)
 {
+  if (!_ostree_sysroot_ensure_writable (sysroot, error))
+    return FALSE;
+
   GLNX_AUTO_PREFIX_ERROR ("Writing out origin file", error);
   GKeyFile *origin =
     new_origin ? new_origin : ostree_deployment_get_origin (deployment);
@@ -2217,7 +2223,10 @@ ostree_sysroot_write_deployments_with_options (OstreeSysroot     *self,
                                                GCancellable      *cancellable,
                                                GError           **error)
 {
-  g_assert (self->loaded);
+  g_assert (self->loadstate == OSTREE_SYSROOT_LOAD_STATE_LOADED);
+
+  if (!_ostree_sysroot_ensure_writable (self, error))
+    return FALSE;
 
   /* Dealing with the staged deployment is quite tricky here. This function is
    * primarily concerned with writing out "finalized" deployments which have
@@ -2374,7 +2383,6 @@ ostree_sysroot_write_deployments_with_options (OstreeSysroot     *self,
 
       if (boot_was_ro_mount)
         {
-          /* TODO: Use new mount namespace.  https://github.com/ostreedev/ostree/issues/1265 */
           if (mount ("/boot", "/boot", NULL, MS_REMOUNT | MS_SILENT, NULL) < 0)
             return glnx_throw_errno_prefix (error, "Remounting /boot read-write");
         }
@@ -2408,8 +2416,10 @@ ostree_sysroot_write_deployments_with_options (OstreeSysroot     *self,
       /* Note equivalent of try/finally here */
       gboolean success = write_deployments_bootswap (self, new_deployments, opts, bootloader,
                                                      &syncstats, cancellable, error);
-      /* Below here don't set GError until the if (!success) check */
-      if (boot_was_ro_mount)
+      /* Below here don't set GError until the if (!success) check.
+       * Note we only bother remounting if a mount namespace isn't in use.
+       * */
+      if (boot_was_ro_mount && !self->mount_namespace_in_use)
         {
           if (mount ("/boot", "/boot", NULL, MS_REMOUNT | MS_RDONLY | MS_SILENT, NULL) < 0)
             {
@@ -2716,6 +2726,9 @@ ostree_sysroot_deploy_tree (OstreeSysroot     *self,
                             GCancellable      *cancellable,
                             GError           **error)
 {
+  if (!_ostree_sysroot_ensure_writable (self, error))
+    return FALSE;
+
   g_autoptr(OstreeDeployment) deployment = NULL;
   if (!sysroot_initialize_deployment (self, osname, revision, origin, override_kernel_argv,
                                       &deployment, cancellable, error))
@@ -2817,6 +2830,9 @@ ostree_sysroot_stage_tree (OstreeSysroot     *self,
                            GCancellable      *cancellable,
                            GError           **error)
 {
+  if (!_ostree_sysroot_ensure_writable (self, error))
+    return FALSE;
+
   OstreeDeployment *booted_deployment = ostree_sysroot_get_booted_deployment (self);
   if (booted_deployment == NULL)
     return glnx_throw (error, "Cannot stage a deployment when not currently booted into an OSTree system");
@@ -3043,6 +3059,9 @@ ostree_sysroot_deployment_set_kargs (OstreeSysroot     *self,
                                      GCancellable      *cancellable,
                                      GError           **error)
 {
+  if (!_ostree_sysroot_ensure_writable (self, error))
+    return FALSE;
+
   /* For now; instead of this do a redeployment */
   g_assert (!ostree_deployment_is_staged (deployment));
 
@@ -3090,6 +3109,8 @@ ostree_sysroot_deployment_set_mutable (OstreeSysroot     *self,
                                        GCancellable      *cancellable,
                                        GError           **error)
 {
+  if (!_ostree_sysroot_ensure_writable (self, error))
+    return FALSE;
 
   if (g_cancellable_set_error_if_cancelled (cancellable, error))
     return FALSE;
index 858673c5d3118c2023c0de4dd676385b1acad602..96670b137b87d3ec4aa5a28ec172f527f2348149 100644 (file)
@@ -40,6 +40,12 @@ typedef enum {
   OSTREE_SYSROOT_DEBUG_TEST_STAGED_PATH = 1 << 3,
 } OstreeSysrootDebugFlags;
 
+typedef enum {
+      OSTREE_SYSROOT_LOAD_STATE_NONE, /* ostree_sysroot_new() was called */
+      OSTREE_SYSROOT_LOAD_STATE_INIT, /* We've loaded basic sysroot state and have an fd */
+      OSTREE_SYSROOT_LOAD_STATE_LOADED, /* We've loaded all of the deployments */
+} OstreeSysrootLoadState;
+
 /**
  * OstreeSysroot:
  * Internal struct
@@ -51,7 +57,8 @@ struct OstreeSysroot {
   int sysroot_fd;
   GLnxLockFile lock;
 
-  gboolean loaded;
+  OstreeSysrootLoadState loadstate;
+  gboolean mount_namespace_in_use; /* TRUE if caller has told us they used CLONE_NEWNS */
   gboolean root_is_ostree_booted; /* TRUE if sysroot is / and we are booted via ostree */
   /* The device/inode for /, used to detect booted deployment */
   dev_t root_device;
@@ -79,6 +86,10 @@ struct OstreeSysroot {
 #define _OSTREE_SYSROOT_DEPLOYMENT_RUNSTATE_DIR "/run/ostree/deployment-state/"
 #define _OSTREE_SYSROOT_DEPLOYMENT_RUNSTATE_FLAG_DEVELOPMENT "unlocked-development"
 
+gboolean
+_ostree_sysroot_ensure_writable (OstreeSysroot      *self,
+                                 GError            **error);
+
 void
 _ostree_sysroot_emit_journal_msg (OstreeSysroot  *self,
                                   const char     *msg);
index 1c9dbf379ca4892fe9877ab2d1fb4e28e8ac9b91..23a069752d5de2af724944828d122499ccbc4ab6 100644 (file)
@@ -226,6 +226,33 @@ ostree_sysroot_new_default (void)
   return ostree_sysroot_new (NULL);
 }
 
+/**
+ * ostree_sysroot_set_mount_namespace_in_use:
+ *
+ * If this function is invoked, then libostree will assume that
+ * a private Linux mount namespace has been created by the process.
+ * The primary use case for this is to have e.g. /sysroot mounted
+ * read-only by default.
+ *
+ * If this function has been called, then when a function which requires
+ * writable access is invoked, libostree will automatically remount as writable
+ * any mount points on which it operates.  This currently is just `/sysroot` and
+ * `/boot`.
+ *
+ * If you invoke this function, it must be before ostree_sysroot_load(); it may
+ * be invoked before or after ostree_sysroot_initialize().
+ *
+ * Since: 2019.7
+ */
+void
+ostree_sysroot_set_mount_namespace_in_use (OstreeSysroot  *self)
+{
+  /* Must be before we're loaded, as otherwise we'd have to close/reopen all our
+     fds, e.g. the repo */
+  g_return_if_fail (self->loadstate < OSTREE_SYSROOT_LOAD_STATE_LOADED);
+  self->mount_namespace_in_use = TRUE;
+}
+
 /**
  * ostree_sysroot_get_path:
  * @self:
@@ -238,6 +265,7 @@ ostree_sysroot_get_path (OstreeSysroot  *self)
   return self->path;
 }
 
+/* Open a directory file descriptor for the sysroot if we haven't yet */
 static gboolean
 ensure_sysroot_fd (OstreeSysroot          *self,
                    GError                **error)
@@ -251,13 +279,51 @@ ensure_sysroot_fd (OstreeSysroot          *self,
   return TRUE;
 }
 
+/* Remount /sysroot read-write if necessary */
+gboolean
+_ostree_sysroot_ensure_writable (OstreeSysroot      *self,
+                                 GError            **error)
+{
+  /* Do nothing if no mount namespace is in use */
+  if (!self->mount_namespace_in_use)
+    return TRUE;
+
+  /* If a mount namespace is in use, ensure we're initialized */
+  if (!ostree_sysroot_initialize (self, error))
+    return FALSE;
+
+  /* If we aren't operating on a booted system, then we don't
+   * do anything with mounts.
+   */
+  if (!self->root_is_ostree_booted)
+    return TRUE;
+
+  /* Check if /sysroot is a read-only mountpoint */
+  struct statvfs stvfsbuf;
+  if (statvfs ("/sysroot", &stvfsbuf) < 0)
+    return glnx_throw_errno_prefix (error, "fstatvfs(/sysroot)");
+  if ((stvfsbuf.f_flag & ST_RDONLY) == 0)
+    return TRUE;
+
+  /* OK, let's remount writable. */
+  if (mount ("/sysroot", "/sysroot", NULL, MS_REMOUNT | MS_RELATIME, "") < 0)
+    return glnx_throw_errno_prefix (error, "Remounting /sysroot read-write");
+
+  /* Reopen our fd */
+  glnx_close_fd (&self->sysroot_fd);
+  if (!ensure_sysroot_fd (self, error))
+    return FALSE;
+
+  return TRUE;
+}
+
 /**
  * ostree_sysroot_get_fd:
  * @self: Sysroot
  *
- * Access a file descriptor that refers to the root directory of this
- * sysroot.  ostree_sysroot_load() must have been invoked prior to
- * calling this function.
+ * Access a file descriptor that refers to the root directory of this sysroot.
+ * ostree_sysroot_initialize() (or ostree_sysroot_load()) must have been invoked
+ * prior to calling this function.
  *
  * Returns: A file descriptor valid for the lifetime of @self
  */
@@ -268,6 +334,22 @@ ostree_sysroot_get_fd (OstreeSysroot *self)
   return self->sysroot_fd;
 }
 
+/**
+ * ostree_sysroot_is_booted:
+ * @self: Sysroot
+ *
+ * Can only be invoked after `ostree_sysroot_initialize()`.
+ * 
+ * Returns: %TRUE iff the sysroot points to a booted deployment
+ * Since: 2019.7
+ */
+gboolean
+ostree_sysroot_is_booted (OstreeSysroot *self)
+{
+  g_return_val_if_fail (self->loadstate >= OSTREE_SYSROOT_LOAD_STATE_INIT, FALSE);
+  return self->root_is_ostree_booted;
+}
+
 gboolean
 _ostree_sysroot_bump_mtime (OstreeSysroot *self,
                             GError       **error)
@@ -798,6 +880,57 @@ ensure_repo (OstreeSysroot  *self,
   return TRUE;
 }
 
+/**
+ * ostree_sysroot_initialize:
+ * @self: sysroot
+ *
+ * Subset of ostree_sysroot_load(); performs basic initialization. Notably, one
+ * can invoke `ostree_sysroot_get_fd()` after calling this function.
+ *
+ * It is not necessary to call this function if ostree_sysroot_load() is
+ * invoked.
+ *
+ * Since: 2019.7
+ */
+gboolean
+ostree_sysroot_initialize (OstreeSysroot  *self,
+                           GError        **error)
+{
+  if (!ensure_sysroot_fd (self, error))
+    return FALSE;
+
+  if (self->loadstate < OSTREE_SYSROOT_LOAD_STATE_INIT)
+    {
+      /* Gather some global state; first if we have the global ostree-booted flag;
+       * we'll use it to sanity check that we found a booted deployment for example.
+       * Second, we also find out whether sysroot == /.
+       */
+      if (!glnx_fstatat_allow_noent (AT_FDCWD, "/run/ostree-booted", NULL, 0, error))
+        return FALSE;
+      const gboolean ostree_booted = (errno == 0);
+
+      { struct stat root_stbuf;
+        if (!glnx_fstatat (AT_FDCWD, "/", &root_stbuf, 0, error))
+          return FALSE;
+        self->root_device = root_stbuf.st_dev;
+        self->root_inode = root_stbuf.st_ino;
+      }
+
+      struct stat self_stbuf;
+      if (!glnx_fstatat (AT_FDCWD, gs_file_get_path_cached (self->path), &self_stbuf, 0, error))
+        return FALSE;
+
+      const gboolean root_is_sysroot =
+        (self->root_device == self_stbuf.st_dev &&
+         self->root_inode == self_stbuf.st_ino);
+
+      self->root_is_ostree_booted = (ostree_booted && root_is_sysroot);
+      self->loadstate = OSTREE_SYSROOT_LOAD_STATE_INIT;
+    }
+
+  return TRUE;
+}
+
 /* Reload the staged deployment from the file in /run */
 gboolean
 _ostree_sysroot_reload_staged (OstreeSysroot *self,
@@ -870,7 +1003,7 @@ ostree_sysroot_load_if_changed (OstreeSysroot  *self,
                                 GCancellable   *cancellable,
                                 GError        **error)
 {
-  if (!ensure_sysroot_fd (self, error))
+  if (!ostree_sysroot_initialize (self, error))
     return FALSE;
 
   /* Here we also lazily initialize the repository.  We didn't do this
@@ -880,34 +1013,6 @@ ostree_sysroot_load_if_changed (OstreeSysroot  *self,
   if (!ensure_repo (self, error))
     return FALSE;
 
-  /* Gather some global state; first if we have the global ostree-booted flag;
-   * we'll use it to sanity check that we found a booted deployment for example.
-   * Second, we also find out whether sysroot == /.
-   */
-  if (!self->loaded)
-    {
-      if (!glnx_fstatat_allow_noent (AT_FDCWD, "/run/ostree-booted", NULL, 0, error))
-        return FALSE;
-      const gboolean ostree_booted = (errno == 0);
-
-      { struct stat root_stbuf;
-        if (!glnx_fstatat (AT_FDCWD, "/", &root_stbuf, 0, error))
-          return FALSE;
-        self->root_device = root_stbuf.st_dev;
-        self->root_inode = root_stbuf.st_ino;
-      }
-
-      struct stat self_stbuf;
-      if (!glnx_fstat (self->sysroot_fd, &self_stbuf, error))
-        return FALSE;
-
-      const gboolean root_is_sysroot =
-        (self->root_device == self_stbuf.st_dev &&
-         self->root_inode == self_stbuf.st_ino);
-
-      self->root_is_ostree_booted = (ostree_booted && root_is_sysroot);
-    }
-
   int bootversion = 0;
   if (!read_current_bootversion (self, &bootversion, cancellable, error))
     return FALSE;
@@ -990,8 +1095,8 @@ ostree_sysroot_load_if_changed (OstreeSysroot  *self,
       ostree_deployment_set_index (deployment, i);
     }
 
-  /* Determine whether we're "physical" or not, the first time we initialize */
-  if (!self->loaded)
+  /* Determine whether we're "physical" or not, the first time we load deployments */
+  if (self->loadstate < OSTREE_SYSROOT_LOAD_STATE_LOADED)
     {
       /* If we have a booted deployment, the sysroot is / and we're definitely
        * not physical.
@@ -1009,13 +1114,14 @@ ostree_sysroot_load_if_changed (OstreeSysroot  *self,
             self->is_physical = TRUE;
         }
       /* Otherwise, the default is FALSE */
+
+      self->loadstate = OSTREE_SYSROOT_LOAD_STATE_LOADED;
     }
 
   self->bootversion = bootversion;
   self->subbootversion = subbootversion;
   self->deployments = deployments;
   deployments = NULL; /* Transfer ownership */
-  self->loaded = TRUE;
   self->loaded_ts = stbuf.st_mtim;
 
   if (out_changed)
@@ -1044,7 +1150,7 @@ ostree_sysroot_get_subbootversion (OstreeSysroot   *self)
 OstreeDeployment *
 ostree_sysroot_get_booted_deployment (OstreeSysroot       *self)
 {
-  g_return_val_if_fail (self->loaded, NULL);
+  g_return_val_if_fail (self->loadstate == OSTREE_SYSROOT_LOAD_STATE_LOADED, NULL);
 
   return self->booted_deployment;
 }
@@ -1060,7 +1166,7 @@ ostree_sysroot_get_booted_deployment (OstreeSysroot       *self)
 OstreeDeployment *
 ostree_sysroot_get_staged_deployment (OstreeSysroot       *self)
 {
-  g_return_val_if_fail (self->loaded, NULL);
+  g_return_val_if_fail (self->loadstate == OSTREE_SYSROOT_LOAD_STATE_LOADED, NULL);
 
   return self->staged_deployment;
 }
@@ -1074,7 +1180,7 @@ ostree_sysroot_get_staged_deployment (OstreeSysroot       *self)
 GPtrArray *
 ostree_sysroot_get_deployments (OstreeSysroot  *self)
 {
-  g_return_val_if_fail (self->loaded, NULL);
+  g_return_val_if_fail (self->loadstate == OSTREE_SYSROOT_LOAD_STATE_LOADED, NULL);
 
   GPtrArray *copy = g_ptr_array_new_with_free_func ((GDestroyNotify)g_object_unref);
   for (guint i = 0; i < self->deployments->len; i++)
@@ -1163,8 +1269,8 @@ ostree_sysroot_get_repo (OstreeSysroot         *self,
  * @self: Sysroot
  *
  * This function is a variant of ostree_sysroot_get_repo() that cannot fail, and
- * returns a cached repository. Can only be called after ostree_sysroot_load()
- * has been invoked successfully.
+ * returns a cached repository. Can only be called after ostree_sysroot_initialize()
+ * or ostree_sysroot_load() has been invoked successfully.
  *
  * Returns: (transfer none): The OSTree repository in sysroot @self.
  *
@@ -1173,7 +1279,7 @@ ostree_sysroot_get_repo (OstreeSysroot         *self,
 OstreeRepo *
 ostree_sysroot_repo (OstreeSysroot *self)
 {
-  g_return_val_if_fail (self->loaded, NULL);
+  g_return_val_if_fail (self->loadstate >= OSTREE_SYSROOT_LOAD_STATE_LOADED, NULL);
   g_assert (self->repo);
   return self->repo;
 }
@@ -1368,6 +1474,10 @@ ostree_sysroot_lock (OstreeSysroot     *self,
 {
   if (!ensure_sysroot_fd (self, error))
     return FALSE;
+
+  if (!_ostree_sysroot_ensure_writable (self, error))
+    return FALSE;
+
   return glnx_make_lock_file (self->sysroot_fd, OSTREE_SYSROOT_LOCKFILE,
                               LOCK_EX, &self->lock, error);
 }
@@ -1391,12 +1501,14 @@ ostree_sysroot_try_lock (OstreeSysroot         *self,
                          gboolean              *out_acquired,
                          GError               **error)
 {
-  g_autoptr(GError) local_error = NULL;
-
   if (!ensure_sysroot_fd (self, error))
     return FALSE;
 
+  if (!_ostree_sysroot_ensure_writable (self, error))
+    return FALSE;
+
   /* Note use of LOCK_NB */
+  g_autoptr(GError) local_error = NULL;
   if (!glnx_make_lock_file (self->sysroot_fd, OSTREE_SYSROOT_LOCKFILE,
                             LOCK_EX | LOCK_NB, &self->lock, &local_error))
     {
@@ -1509,7 +1621,7 @@ ostree_sysroot_init_osname (OstreeSysroot       *self,
                             GCancellable        *cancellable,
                             GError             **error)
 {
-  if (!ensure_sysroot_fd (self, error))
+  if (!_ostree_sysroot_ensure_writable (self, error))
     return FALSE;
 
   const char *deploydir = glnx_strjoina ("ostree/deploy/", osname);
index 502cd75020a6a0a3e41048c6262dc11fc5025290..af8192fc409464903de04cc0bafbbd44c3d3d3b0 100644 (file)
@@ -41,12 +41,22 @@ OstreeSysroot* ostree_sysroot_new (GFile *path);
 _OSTREE_PUBLIC
 OstreeSysroot* ostree_sysroot_new_default (void);
 
+_OSTREE_PUBLIC
+void ostree_sysroot_set_mount_namespace_in_use (OstreeSysroot  *self);
+
 _OSTREE_PUBLIC
 GFile *ostree_sysroot_get_path (OstreeSysroot *self);
 
+_OSTREE_PUBLIC
+gboolean ostree_sysroot_is_booted (OstreeSysroot *self);
+
 _OSTREE_PUBLIC
 int ostree_sysroot_get_fd (OstreeSysroot *self);
 
+_OSTREE_PUBLIC
+gboolean ostree_sysroot_initialize (OstreeSysroot  *self,
+                                    GError        **error);
+
 _OSTREE_PUBLIC
 gboolean ostree_sysroot_load (OstreeSysroot  *self,
                               GCancellable   *cancellable,
@@ -90,6 +100,10 @@ GFile * ostree_sysroot_get_deployment_origin_path (GFile   *deployment_path);
 
 _OSTREE_PUBLIC
 gboolean ostree_sysroot_lock (OstreeSysroot  *self, GError **error);
+
+_OSTREE_PUBLIC
+gboolean ostree_sysroot_lock_with_mount_namespace (OstreeSysroot  *self, GError **error);
+
 _OSTREE_PUBLIC
 gboolean ostree_sysroot_try_lock (OstreeSysroot         *self,
                                   gboolean              *out_acquired,
index 4b72f3995b2647e67d2d88ec9fcce92227585193..a044cef23fccdc943d3ca933c8d2b1c6754713e9 100644 (file)
@@ -27,6 +27,7 @@
 
 #include <stdlib.h>
 #include <string.h>
+#include <sys/statvfs.h>
 
 #include "ot-main.h"
 #include "ostree.h"
@@ -434,10 +435,46 @@ ostree_admin_option_context_parse (GOptionContext *context,
     sysroot_path = g_file_new_for_path (opt_sysroot);
 
   g_autoptr(OstreeSysroot) sysroot = ostree_sysroot_new (sysroot_path);
+  if (!ostree_sysroot_initialize (sysroot, error))
+    return FALSE;
   g_signal_connect (sysroot, "journal-msg", G_CALLBACK (on_sysroot_journal_msg), NULL);
 
   if ((flags & OSTREE_ADMIN_BUILTIN_FLAG_UNLOCKED) == 0)
     {
+      /* If we're requested to lock the sysroot, first check if we're operating
+       * on a booted (not physical) sysroot.  Then find out if the /sysroot
+       * subdir is a read-only mount point, and if so, create a new mount
+       * namespace and tell the sysroot that we've done so. See the docs for
+       * ostree_sysroot_set_mount_namespace_in_use().
+       *
+       * This is a conservative approach; we could just always
+       * unshare() too.
+       */
+      if (ostree_sysroot_is_booted (sysroot))
+        {
+          int sysroot_fd = ostree_sysroot_get_fd (sysroot);
+          g_assert_cmpint (sysroot_fd, !=, -1);
+
+          glnx_autofd int sysroot_subdir_fd = glnx_opendirat_with_errno (sysroot_fd, "sysroot", TRUE);
+          if (sysroot_subdir_fd < 0)
+            {
+              if (errno != ENOENT)
+                return glnx_throw_errno_prefix (error, "opendirat");
+            }
+          else if (getuid () == 0)
+            {
+              struct statvfs stvfs;
+              if (fstatvfs (sysroot_subdir_fd, &stvfs) < 0)
+                return glnx_throw_errno_prefix (error, "fstatvfs");
+              if (stvfs.f_flag & ST_RDONLY)
+                {
+                  if (unshare (CLONE_NEWNS) < 0)
+                    return glnx_throw_errno_prefix (error, "preparing writable sysroot: unshare (CLONE_NEWNS)");
+                  ostree_sysroot_set_mount_namespace_in_use (sysroot);
+                }
+            }
+        }
+
       /* Released when sysroot is finalized, or on process exit */
       if (!ot_admin_sysroot_lock (sysroot, error))
         return FALSE;
index 5e6d23d3aea5783bd80e13e88caed2aa058d2e5d..326b104f038c935388eb08b3a4fe74aaa64584d6 100644 (file)
@@ -26,6 +26,7 @@
 #include <string.h>
 #include <stdio.h>
 #include <stdarg.h>
+#include <stdbool.h>
 #include <stdint.h>
 #include <sys/param.h>
 #include <sys/mount.h>
 #include <err.h>
 #include <errno.h>
 
+#include <glib.h>
+
 #include "ostree-mount-util.h"
+#include "glnx-backport-autocleanups.h"
 
 static void
-do_remount (const char *target)
+do_remount (const char *target,
+            bool        writable)
 {
   struct stat stbuf;
   if (lstat (target, &stbuf) < 0)
@@ -54,20 +59,41 @@ do_remount (const char *target)
   struct statvfs stvfsbuf;
   if (statvfs (target, &stvfsbuf) == -1)
     return;
-  /* If no read-only flag, skip it */
-  if ((stvfsbuf.f_flag & ST_RDONLY) == 0)
+
+  const bool currently_writable = ((stvfsbuf.f_flag & ST_RDONLY) == 0);
+  if (writable == currently_writable)
     return;
-  /* It's a mounted, read-only fs; remount it */
-  if (mount (target, target, NULL, MS_REMOUNT | MS_SILENT, NULL) < 0)
+
+  int mnt_flags = MS_REMOUNT | MS_SILENT;
+  if (!writable)
+    mnt_flags |= MS_RDONLY;
+  if (mount (target, target, NULL, mnt_flags, NULL) < 0)
     {
       /* Also ignore EINVAL - if the target isn't a mountpoint
        * already, then assume things are OK.
        */
-      if (errno != EINVAL)
-        err (EXIT_FAILURE, "failed to remount %s", target);
+      if (errno != EINVAL)  
+        err (EXIT_FAILURE, "failed to remount(%s) %s", writable ? "rw" : "ro", target);
+      else
+        return;
     }
-  else
-    printf ("Remounted: %s\n", target);
+
+  printf ("Remounted %s: %s\n", writable ? "rw" : "ro", target);
+}
+
+static bool
+sysroot_is_configured_ro (void)
+{
+  struct stat stbuf;
+  static const char config_path[] = "/ostree/repo/config";
+  if (stat (config_path, &stbuf) != 0)
+    return false;
+
+  g_autoptr(GKeyFile) keyfile = g_key_file_new ();
+  if (!g_key_file_load_from_file (keyfile, config_path, 0, NULL))
+    return false;
+
+  return g_key_file_get_boolean (keyfile, "sysroot", "readonly", NULL);
 }
 
 int
@@ -95,8 +121,38 @@ main(int argc, char *argv[])
       exit (EXIT_SUCCESS);
     }
 
-  do_remount ("/sysroot");
-  do_remount ("/var");
+  /* Query the repository configuration - this is an operating system builder
+   * choice.
+   * */
+  const bool sysroot_readonly = sysroot_is_configured_ro ();
+
+  /* Mount the sysroot read-only if we're configured to do so.
+   * Note we only get here if / is already writable.
+   */
+  do_remount ("/sysroot", !sysroot_readonly);
+
+  if (sysroot_readonly)
+    {
+      /* Now, /etc is not normally a bind mount, but remounting the
+       * sysroot above made it read-only since it's on the same filesystem.
+       * Make it a self-bind mount, so we can then mount it read-write.
+       */
+      if (mount ("/etc", "/etc", NULL, MS_BIND, NULL) < 0)
+        err (EXIT_FAILURE, "failed to make /etc a bind mount");
+      do_remount ("/etc", true);
+    }
+
+  /* If /var was created as as an OSTree default bind mount (instead of being a separate filesystem)
+    * then remounting the root mount read-only also remounted it.
+    * So just like /etc, we need to make it read-write by default.
+    * If it was a separate filesystem, we expect it to be writable anyways,
+    * so it doesn't hurt to remount it if so.
+    *
+    * And if we started out with a writable system root, then we need
+    * to ensure that the /var bind mount created by the systemd generator
+    * is writable too.
+    */
+  do_remount ("/var", true);
 
   exit (EXIT_SUCCESS);
 }